﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Blade.Metadata.CliLoader;
using Blade.IO;
using Blade.Metadata.CliLoader.Metadata;

namespace Blade.Metadata.PeLoader {
	sealed class PeFile : ICliFileReader {
		const int DOS_HEADER_LFANEW_OFFSET = 0x3C; // ECMA-335 Partition II, §25.2.1
		static readonly char[] PE_SIGNATURE = new char[] { 'P', 'E', '\0', '\0' }; // Partition II, §25.2.1

		PeHeader peHeader;
		CliFileHeader cliFileHeader;
		CliSectionHeader cliSectionHeader;
		internal CliMetadataHeader cliMetadataHeader;
		CursorStream fileStream;

		public PeFile(CursorStream stream, IMetadataLoaderContext context) {
			this.fileStream = stream;
			var reader = stream.AsBinaryReader();
			#region PE Header
			stream.Position = DOS_HEADER_LFANEW_OFFSET;
			var peHeaderOffset = reader.ReadInt32();
			stream.Position = peHeaderOffset;
			var pesig = reader.ReadChars(4);
			context.Assert(() => reader.Position() == peHeaderOffset + 4);
			context.Assert(() => pesig.SequenceEqual(PE_SIGNATURE));
			peHeader = new PeHeader(reader, context);
			#endregion
			#region CliFileHeader
			stream.Position = ConvertRVAToOffset(peHeader.DataDirectory[14].RVA);
			this.cliFileHeader = new CliFileHeader(reader, context);
			var metadataSection = stream.CreateCursor(
				ConvertRVAToOffset(cliFileHeader.MetadataRoot.RVA),
				(int)cliFileHeader.MetadataRoot.Size,
				false
			);
			cliSectionHeader = new CliSectionHeader(metadataSection.AsBinaryReader(), context);
			foreach (var header in cliSectionHeader.StreamHeaders) {
				header.Cursor = metadataSection.CreateCursor(
					header.Offset,
					header.Size,
					false
				);
			}
			#endregion
			var metadataStream = cliSectionHeader.StreamHeaders["#~"].Cursor;
			cliMetadataHeader = new CliMetadataHeader(metadataStream, context);
			var offset = metadataStream.Position;
			foreach (var table in cliMetadataHeader.Tables) {
				var rowSize = CalculateRowSize(table.Fields);
				var tableSize = table.RowCount * rowSize;
				table.Reader =
					new PeTableReader(
						metadataStream.CreateCursor(offset, tableSize, false),
						this
					);
				offset += tableSize;
			}
		}

		private int CalculateRowSize(CliFieldType[] fields) {
			return fields.Sum((f) => GetFieldSize(f));
		}

		private int GetFieldSize(CliFieldType field) {
			switch (field) {
				case CliFieldType.Int32:
					return 4;
				case CliFieldType.Int16:
					return 2;
				case CliFieldType.Byte:
					return 1;
				case CliFieldType.StringRef:
					return cliMetadataHeader.HeapSizes[0] ? 4 : 2;
				case CliFieldType.GuidRef:
					return cliMetadataHeader.HeapSizes[1] ? 4 : 2;
				case CliFieldType.BlobRef:
					return cliMetadataHeader.HeapSizes[2] ? 4 : 2;
				case CliFieldType.ResolutionScope:
					return CliTaggedField.ResolutionScope.GetSize(this);
				case CliFieldType.TypeDefOrRef:
					return CliTaggedField.TypeDefOrRef.GetSize(this);
				case CliFieldType.HasConstant:
					return CliTaggedField.HasConstant.GetSize(this);
				case CliFieldType.HasCustomAttribute:
					return CliTaggedField.HasCustomAttribute.GetSize(this);
				case CliFieldType.HasFieldMarshal:
					return CliTaggedField.HasFieldMarshal.GetSize(this);
				case CliFieldType.HasDeclSecurity:
					return CliTaggedField.HasDeclSecurity.GetSize(this);
				case CliFieldType.MemberRefParent:
					return CliTaggedField.MemberRefParent.GetSize(this);
				case CliFieldType.HasSemantics:
					return CliTaggedField.HasSemantics.GetSize(this);
				case CliFieldType.MethodDefOrRef:
					return CliTaggedField.MethodDefOrRef.GetSize(this);
				case CliFieldType.MemberForwarded:
					return CliTaggedField.MemberForwarded.GetSize(this);
				case CliFieldType.Implementation:
					return CliTaggedField.Implementation.GetSize(this);
				case CliFieldType.CustomAttributeType:
					return CliTaggedField.CustomAttributeType.GetSize(this);
				case CliFieldType.TypeOrMethodDef:
					return CliTaggedField.TypeOrMethodDef.GetSize(this);
				default:
					throw new NotSupportedException();
			}
		}

		long ConvertRVAToOffset(RVA rva) {
			foreach (var section in peHeader.SectionHeaders) {
				var start = section.VirtualAddress;
				var end = start + section.VirtualSize;
				if (rva.Value >= start && rva.Value <= end) {
					return section.OffsetToRawData + (rva.Value - start);
				}
			}
			throw new NotImplementedException();
		}

		#region ICliFileReader Members

		public TableReader OpenTable(MetadataTable table) {
			return 				cliMetadataHeader.Tables[table].Reader;
		}

		public int GetRowCount(MetadataTable tableId) {
			return cliMetadataHeader.Tables[tableId].RowCount;
		}

		public string GetString(int index) {
			throw new NotImplementedException();
		}

		public string GetUserString(int index) {
			throw new NotImplementedException();
		}

		public Guid GetGuid(int index) {
			throw new NotImplementedException();
		}

		public byte[] GetBlob(int index) {
			throw new NotImplementedException();
		}

		#endregion
	}
}
